package store.lunangangster.engine.impl.db;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.apache.ibatis.session.SqlSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import store.lunangangster.engine.NodeFlowException;
import store.lunangangster.engine.impl.db.cache.CachedEntity;
import store.lunangangster.engine.impl.db.cache.EntityCache;
import store.lunangangster.engine.impl.entity.Entity;
import store.lunangangster.engine.impl.util.EntityUtil;

/**
 * DbSqlSession中实现了Entity事务
 *
 * @author cwk
 * @version 1.0
 */
public class DbSqlSession implements Session {

  private static final Logger log = LoggerFactory.getLogger(DbSqlSession.class);

  protected DbSqlSessionFactory dbSqlSessionFactory;
  protected SqlSession sqlSession;
  protected boolean autoCommit;

  // for transaction
  protected Map<Class<? extends Entity>, Map<String, Entity>> insertMap = new HashMap<>();
  protected Map<Class<? extends Entity>, Map<String, Entity>> deleteMap = new HashMap<>();
  protected Map<Class<? extends Entity>, List<Entity>> updateMap; // create when used

  // transaction cache, for before DbSqlSession commit
  protected EntityCache entityCache;

  public DbSqlSession(DbSqlSessionFactory dbSqlSessionFactory) {
    this.dbSqlSessionFactory = dbSqlSessionFactory;
    this.entityCache = dbSqlSessionFactory.getEntityCache();
  }

  public void batchInsert(Collection<? extends Entity> entities) {
    if (!entities.isEmpty()) {
      for (Entity entity : entities) {
        insert(entity);
      }
    }
  }

  public void insert(Entity entity) {
    if (entity == null) {
      return;
    }
    if (EntityUtil.isNeededId(entity)) {
      entity.setId(dbSqlSessionFactory.getIdGenerator().nextId());
    }
    // add to transaction
    if (!insertMap.containsKey(entity.getClass())) {
      insertMap.put(entity.getClass(), new LinkedHashMap<>());  // Linked for order
    }
    insertMap.get(entity.getClass()).put(entity.getId(), entity);
    putToCache(entity);
    // wait for commit
    entity.setInsert(true);
  }

  public void update(Entity entity) {
    if (entity == null) {
      return;
    }
    if (!updateMap.containsKey(entity.getClass())) {
      updateMap.put(entity.getClass(), new ArrayList<>());
    }
    updateMap.get(entity.getClass()).add(entity);
    entity.setUpdate(true);
  }

  public <T extends Entity> void delete(Class<T> clazz, String id) {
    if (id == null || clazz == null) {
      return;
    }
    // add to transaction
    if (!deleteMap.containsKey(clazz)) {
      deleteMap.put(clazz, new LinkedHashMap<>());  // Linked for order
    }
    T entity = selectById(clazz, id);
    if (entity != null) {
      deleteMap.get(clazz).put(id, entity);
      // removeFromCache(entity);   // no need to remove
      // wait for commit
      entity.setDelete(true);
    }
  }

  public <T extends Entity> void delete(T entity) {
    if (EntityUtil.hasNoId(entity)) {
      return;
    }
    Class<? extends Entity> clazz = entity.getClass();
    // add to transaction
    if (!deleteMap.containsKey(clazz)) {
      deleteMap.put(clazz, new LinkedHashMap<>());  // Linked for order
    }
    deleteMap.get(clazz).put(entity.getId(), entity);
    // removeFromCache(entity);   // no need to remove
    // wait for commit
    entity.setDelete(true);
  }

  /**
   * select {@link Entity} using cache first. if not found, then
   * find in DB
   * @param clazz the Class who extends {@link Entity}
   * @param id  Entity's id
   * @param <T> extends Entity
   * @return  Entity Object
   */
  public <T extends Entity> T selectById(Class<T> clazz, String id) {
    if (id == null || clazz == null) {
      return null;
    }
    // first query in cache, if in the same transaction
    // otherwise, query in DB
    T r = Optional.ofNullable(findInCache(clazz, id))
        .orElseGet(() -> {
          String selectStatement = dbSqlSessionFactory.getStatements()
              .getSelectStatement(clazz);
          T e = sqlSession.selectOne(selectStatement, id);
          if (e != null) {
            putToCache(e);
          }
          return e;
        });
    if (r == null || r.isDelete()) {
      return null;
    }
    return r;
  }

  /**
   * 查询数据库或缓存，并调用{@link #loadFromCacheOrStore(Entity)}
   */
  public Object selectOne(String statement, Object parameter) {
    Object result = sqlSession.selectOne(statement, parameter);
    if (result instanceof Entity) {
      return loadFromCacheOrStore((Entity) result);
    }
    return sqlSession.selectOne(statement, parameter);
  }

  /**
   * 直接查询数据库
   */
  @SuppressWarnings("rawtypes")
  public List selectList(String statement, Object parameter) {
    List<Object> objects = sqlSession.selectList(statement, parameter);
    return loadFromCacheOrStore(objects);
  }

  /**
   * 该方法会将所有在DbSqlSession中进行的DML操作提交给SqlSession。该类的实现中，此方法不会进行commit
   */
  @Override
  public void flush() {
    // 判断是否有Entity的状态发生变化，有则加入到UpdateMap
    determineUpdateMap();
    // 去掉既需要插入，又需要删除的Entity所对应的DML操作
    removeUnnecessaryDML();

    log.debug("start flushing: {}", this);

    flushInsert();
    flushUpdate();
    flushDelete();

    log.debug("flush over: {}", this);
  }

  private void determineUpdateMap() {
    updateMap = new LinkedHashMap<>();
    Map<Class<? extends Entity>, Map<String, CachedEntity>> cachedEntities = entityCache.getAllCachedEntities();

    for (Class<? extends Entity> clazz : cachedEntities.keySet()) {
      List<Entity> updateList = new ArrayList<>();
      for (CachedEntity cachedEntity : cachedEntities.get(clazz).values()) {
        if (!isEntityInsert(cachedEntity) && !isEntityDelete(cachedEntity) && cachedEntity.hasChanged()) {
          updateList.add(cachedEntity.getEntity());
        }
      }
      if (!updateList.isEmpty()) {
        updateMap.put(clazz, updateList);
      }
    }
  }


  private boolean isEntityInsert(CachedEntity cachedEntity) {
    return cachedEntity.getEntity().isInsert();
  }

  private boolean isEntityDelete(CachedEntity cachedEntity) {
    return cachedEntity.getEntity().isDelete();
  }

  private void removeUnnecessaryDML() {
    for (Class<? extends Entity> clazz : deleteMap.keySet()) {
      Set<String> idSet = new HashSet<>();
      Iterator<Entity> iterator = deleteMap.get(clazz).values().iterator();
      while (iterator.hasNext()) {
        Entity entity = iterator.next();
        if (!idSet.contains(entity.getId())) {
          idSet.add(entity.getId());
        } else {
          iterator.remove();  // remove duplicate Entity
        }
      }

      if (insertMap.containsKey(clazz)) {
        for (String id : idSet) {
          if (insertMap.get(clazz).containsKey(id)) {
            insertMap.get(clazz).remove(id);
            deleteMap.get(clazz).remove(id);
          }
        }
      }
    }
  }

  private void flushInsert() {
    if (insertMap.size() == 0) {
      return;
    }

    // keep entity order for foreign
    for (Class<? extends Entity> clazz : EntityDependencyOrder.INSERT_ORDER) {
      if (insertMap.containsKey(clazz)) {
        flushClassEntitiesInsert(clazz);
      }
    }
    // clear the bucket
    insertMap.clear();
  }

  private void flushClassEntitiesInsert(Class<? extends Entity> clazz) {
    if (dbSqlSessionFactory.isBatchInsert() && EntityUtil.allowBatchInsert(clazz)) {
      flushBatchInsert(clazz, insertMap.get(clazz));
    } else {
      flushRegularInsert(clazz, insertMap.get(clazz));
    }
  }

  private void flushRegularInsert(Class<? extends Entity> clazz, Map<String, Entity> objectMap) {
    String statement = dbSqlSessionFactory.getStatements().getInsertStatement(clazz);

    if (statement == null) {
      throw new NodeFlowException("找不到" + clazz.getName() + "对应的insertStatement");
    }

    for (Entity entity : objectMap.values()) {
      sqlSession.insert(statement, entity);
    }
  }

  private void flushBatchInsert(Class<? extends Entity> clazz, Map<String, Entity> objectMap) {
    String statement = dbSqlSessionFactory.getStatements().getBatchInsertStatement(clazz);

    if (statement == null) {
      throw new NodeFlowException("找不到" + clazz.getName() + "对应的batchInsertStatement");
    }
    if (!objectMap.values().isEmpty()) {
      List<Entity> list = new ArrayList<>(objectMap.values());
      sqlSession.insert(statement, list);
    }
  }

  private void flushUpdate() {
    if (updateMap.size() == 0) {
      return;
    }

    for (Class<? extends Entity> clazz : updateMap.keySet()) {
      String statement = dbSqlSessionFactory.getStatements().getUpdateStatement(clazz);

      if (statement == null) {
        throw new NodeFlowException("找不到" + clazz.getName() + "对应的updateStatement");
      }

      for (Entity entity : updateMap.get(clazz)) {
        sqlSession.update(statement, entity);
      }
    }

    // clear the bucket
    updateMap.clear();
  }

  private void flushDelete() {
    if (deleteMap.size() == 0) {
      return;
    }

    // reverse travel
    for (int i = EntityDependencyOrder.INSERT_ORDER.size() - 1; i >= 0; i--) {
      Class<? extends Entity> clazz = EntityDependencyOrder.INSERT_ORDER.get(i);
      if (deleteMap.containsKey(clazz)) {
        String statement = dbSqlSessionFactory.getStatements().getDeleteStatement(clazz);

        if (statement == null) {
          throw new NodeFlowException("找不到" + clazz.getName() + "对应的deleteStatement");
        }
        for (Entity entity : deleteMap.get(clazz).values()) {
          sqlSession.delete(statement, entity);
        }
        deleteMap.get(clazz).clear();
      }
    }


    // clear the bucket
    deleteMap.clear();
  }

  @Override
  public void close() {
    sqlSession.close();
  }

  public SqlSession getSqlSession() {
    return sqlSession;
  }

  public boolean isAutoCommit() {
    return autoCommit;
  }

  protected void putToCache(Entity entity) {
    entityCache.put(entity.getClass(), entity);
  }

  protected void removeFromCache(Entity entity) {
    entityCache.remove(entity.getClass(), entity.getId());
  }

  protected <T extends Entity> T findInCache(Class<T> clazz, String id) {
    return entityCache.get(clazz, id);
  }

  /**
   * 将entity加载到Cache中，如果当前entity的class和id组成的key在Cache中存在，
   * 则返回已缓存的对象；否则认为当前entity是第一次加载到Cache中，并且返回当前entity
   * <p>总之，保证调用该方法之后，entity对象或entity的key对应的对象一定会缓存
   * @param entity  需要加载到Cache中或从Cache中获取的entity
   * @return  entity本体或由entity的class和id组成的key在缓存中已经存在的对象
   */
  protected Entity loadFromCacheOrStore(Entity entity) {
    Entity cachedEntity = findInCache(entity.getClass(), entity.getId());
    if (cachedEntity != null) {
      return cachedEntity;
    }
    putToCache(entity);
    return entity;
  }

  /**
   * 将entity集合加载到Cache中，如果当前entity的class和id组成的key在Cache中存在，
   * 则返回已缓存的对象；否则认为当前entity是第一次加载到Cache中，并且返回当前entity
   * <p>总之，保证调用该方法之后，entity对象或entity的key对应的对象一定会缓存
   * @param entities  需要加载到Cache中或从Cache中获取的entity集合
   * @return  entity本体或由entity的class和id组成的key在缓存中已经存在的对象集合
   */
  @SuppressWarnings("rawtypes")
  protected List loadFromCacheOrStore(List<Object> entities) {
    if (entities == null || entities.isEmpty()) {
      return entities;
    }
    if (!(entities.get(0) instanceof Entity)) {
      return entities;
    }
    List<Entity> result = new ArrayList<>(entities.size());
    for (Object o : entities) {
      Entity entity = (Entity) o;
      Entity cachedEntity = loadFromCacheOrStore(entity);
      // 由于loadFromCacheOrStore的返回值不一定是entity本身，还可能是缓存中的对象。所以这里需要重新接收
      result.add(cachedEntity);
    }
    return result;
  }

}
